<template>
  <div class="dual-panel" ref="rootEl">
    <div class="sidebar" ref="sidebarEl"></div>
  <div class="main">
      <div class="viewport" ref="vpLeftEl" @contextmenu.prevent></div>
      <div class="viewport" ref="vpRightEl" @contextmenu.prevent></div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import {
  RenderingEngine,
  Enums,
  setVolumesForViewports,
  volumeLoader,
  getRenderingEngine,
  utilities as csUtils,
  init as coreInit,
  metaData
} from '@cornerstonejs/core'
import * as cornerstoneTools from '@cornerstonejs/tools'
import dicomImageLoader from '@cornerstonejs/dicom-image-loader'
import { api as dicomwebApi } from 'dicomweb-client'
import { DicomDataService } from '@/api/pacs/dicom/dicomService.js'
import * as dicomParser from 'dicom-parser'

// 兼容性处理：确保requestAnimationFrame可用且稳定
(function() {
  // 保存原始方法
  const nativeRAF = window.requestAnimationFrame;
  const nativeCAF = window.cancelAnimationFrame;
  
  // 降级方案
  const fallbackRAF = function(callback) {
    return setTimeout(function() {
      callback(Date.now());
    }, 1000 / 60);
  };
  
  const fallbackCAF = function(id) {
    clearTimeout(id);
  };
  
  // 检测原生方法是否可用且正常工作
  let rafSupported = false;
  let lastTime = 0;
  
  try {
    const testRaf = nativeRAF(function(time) {
      rafSupported = time > lastTime;
    });
    nativeCAF(testRaf);
  } catch (e) {
    rafSupported = false;
  }
  
  // 使用稳定的版本
  window.requestAnimationFrame = rafSupported && nativeRAF ? nativeRAF : fallbackRAF;
  window.cancelAnimationFrame = rafSupported && nativeCAF ? nativeCAF : fallbackCAF;
  
  console.log('RAF compatibility check:', rafSupported ? 'Using native RAF' : 'Using fallback RAF');
})();

// 可选外部数据输入
const props = defineProps({
  imageIds: { type: Array, default: () => [] },
  mode: { type: String, default: 'qiniu' }, // 'dicomweb' | 'qiniu' | 'dicomweb-images'
  volumeId: { type: String, default: '' },
})

const { ToolGroupManager, Enums: csToolsEnums, ReferenceLinesTool, ZoomTool } = cornerstoneTools
const { ViewportType } = Enums

// Rendering setup ids
const renderingEngineId = 'RE_DUAL_VIEW_SERIES'
const viewportIdLeft = 'VP_LEFT_CORONAL'
const viewportIdRight = 'VP_RIGHT_AXIAL'
const toolGroupId = 'TG_DUAL_VIEW_SERIES'

// Data source (OHIF demo server)
const wadoRsRoot = 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb'

// Example series definitions (public TCIA studies)
const SERIES = [
  {
    title: '序列 1',
    StudyInstanceUID:
      '1.3.6.1.4.1.14519.5.2.1.3671.4754.298665348758363466150039312520',
    SeriesInstanceUID:
      '1.3.6.1.4.1.14519.5.2.1.3671.4754.130082334601796964681190916115',
  },
  {
    title: '序列 2',
    StudyInstanceUID:
      '1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339',
    SeriesInstanceUID:
      '1.3.6.1.4.1.14519.5.2.1.7009.2403.367700692008930469189923116409',
  },
  {
    title: '序列 3',
    StudyInstanceUID:
      '1.3.6.1.4.1.14519.5.2.1.7310.5101.860473186348887719777907797922',
    SeriesInstanceUID:
      '1.3.6.1.4.1.14519.5.2.1.7310.5101.118448790473786779809042654947',
  },
]

// Refs
const rootEl = ref(null)
const sidebarEl = ref(null)
const vpLeftEl = ref(null)
const vpRightEl = ref(null)

// 左侧定位线 DOM
let locatorLineEl = null

let currentVolumeId = null
let wheelHandler = null
let dragging = false
let lastY = 0
let isStackMode = false

// 本地 Qiniu 元数据缓存与 provider（高优先级）
let qiniuProviderRegistered = false
const qiniuMetaCache = {}
function addQiniuMeta(type, imageId, data) {
  if (!data) return
  const t = qiniuMetaCache[type] || (qiniuMetaCache[type] = {})
  t[imageId] = data
}
function registerLocalQiniuMetaProvider() {
  if (qiniuProviderRegistered) return
  const provider = (type, imageId) => {
    const t = qiniuMetaCache[type]
    if (t && t[imageId]) return t[imageId]
    return undefined
  }
  // 设高优先级，确保被首先查询
  try { metaData.addProvider(provider, 2000); } catch (e) {}
  qiniuProviderRegistered = true
}

async function prefetchQiniuMetadata(ids) {
  registerLocalQiniuMetaProvider()
  const ds = new DicomDataService()
  const types = [
    'imagePixelModule',
    'voiLutModule',
    'generalSeriesModule',
    'imagePlaneModule',
    'generalImageModule',
    'sopCommonModule',
    'transferSyntax',
  ]
  // 从 dataSet 构建最小 imagePixelModule 等元数据的兜底方案
  const buildFallbackMeta = (id, dataSet) => {
    try {
      // imagePixelModule（最关键，Volume 构建会依赖）
      const imagePixelModule = {
        samplesPerPixel: dataSet.uint16('x00280002') || 1,
        photometricInterpretation: dataSet.string('x00280004') || 'MONOCHROME2',
        rows: dataSet.uint16('x00280010') || 0,
        columns: dataSet.uint16('x00280011') || 0,
        bitsAllocated: dataSet.uint16('x00280100') || 16,
        bitsStored: dataSet.uint16('x00280101') || 12,
        highBit: dataSet.uint16('x00280102') || 11,
        pixelRepresentation: dataSet.uint16('x00280103') || 0,
      }
      addQiniuMeta('imagePixelModule', id, imagePixelModule)

      // imagePlaneModule（用于几何信息）
      const pixelSpacingStr = dataSet.string('x00280030')
      const imageOrientationPatientStr = dataSet.string('x00200037')
      const imagePositionPatientStr = dataSet.string('x00200032')
      const sliceThicknessStr = dataSet.string('x00180050')
      const spacingBetweenSlicesStr = dataSet.string('x00180088')
      const imagePlaneModule = {
        pixelSpacing: pixelSpacingStr ? pixelSpacingStr.split('\\').map(parseFloat) : undefined,
        imageOrientationPatient: imageOrientationPatientStr ? imageOrientationPatientStr.split('\\').map(parseFloat) : undefined,
        imagePositionPatient: imagePositionPatientStr ? imagePositionPatientStr.split('\\').map(parseFloat) : undefined,
        sliceThickness: sliceThicknessStr ? parseFloat(sliceThicknessStr) : undefined,
        spacingBetweenSlices: spacingBetweenSlicesStr ? parseFloat(spacingBetweenSlicesStr) : undefined,
      }
      addQiniuMeta('imagePlaneModule', id, imagePlaneModule)

      // generalImageModule / generalSeriesModule / sopCommonModule（按需填充少量字段）
      const sopInstanceUid = dataSet.string('x00080018')
      const seriesInstanceUid = dataSet.string('x0020000e')
      const studyInstanceUid = dataSet.string('x0020000d')
      addQiniuMeta('sopCommonModule', id, { sopInstanceUID: sopInstanceUid })
      addQiniuMeta('generalSeriesModule', id, { seriesInstanceUID: seriesInstanceUid, studyInstanceUID: studyInstanceUid })
      addQiniuMeta('generalImageModule', id, { })

      // transferSyntax（影响解码器选择）
      const transferSyntax = dataSet.string('x00020010') || dataSet.string('x00100010')
      if (transferSyntax) {
        addQiniuMeta('transferSyntax', id, { transferSyntaxUID: transferSyntax })
      }
    } catch (e) {
      // 兜底失败不阻塞
    }
  }
  for (const id of ids) {
    try {
      const url = id.replace('qiniu:', '').split('?')[0]
      const arrayBuffer = await ds.fetchDicomFromQiniu(url)
      const dataSet = dicomParser.parseDicom(new Uint8Array(arrayBuffer))
      for (const t of types) {
        const data = dicomImageLoader.wadouri.metadataForDataset(t, id, dataSet)
        if (data) {
          addQiniuMeta(t, id, data)
        }
      }
      // 若关键模块缺失，使用兜底构建，避免 Volume 构建时 getMetaData 返回 undefined
      if (!qiniuMetaCache['imagePixelModule']?.[id]) {
        buildFallbackMeta(id, dataSet)
      }
    } catch (e) {
      console.warn('[Qiniu] 预取元数据失败:', id, e)
    }
  }
}
let stackIndex = 0
let stackCount = 0

async function initEnv() {
  try { await coreInit(); console.log('Cornerstone core initialized successfully'); } catch (e) { console.error('Cornerstone core init failed:', e); }
  try { dicomImageLoader.init(); console.log('DICOM image loader initialized successfully'); } catch (e) { console.error('DICOM image loader init failed:', e); }
  try { cornerstoneTools.init(); console.log('Cornerstone tools initialized successfully'); } catch (e) { console.error('Cornerstone tools init failed:', e); }
}

function setCtTransferFunctionForVolumeActor({ volumeActor }) {
  const windowWidth = 400
  const windowCenter = 40
  const lower = windowCenter - windowWidth / 2.0
  const upper = windowCenter + windowWidth / 2.0
  volumeActor.getProperty().getRGBTransferFunction(0).setMappingRange(lower, upper)
}

async function createImageIdsAndCacheMetaData({ StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID = null }) {
  if (props.mode === 'qiniu') {
    // 在 qiniu 模式下不走 dicomweb，避免无用报错
    return []
  }
  const SOP_INSTANCE_UID = '00080018'
  const SERIES_INSTANCE_UID = '0020000E'
  const client = new dicomwebApi.DICOMwebClient({ url: wadoRsRoot })
  let instances = await client.retrieveSeriesMetadata({ studyInstanceUID: StudyInstanceUID, seriesInstanceUID: SeriesInstanceUID })

  if (SOPInstanceUID) {
    instances = instances.filter((instance) => instance[SOP_INSTANCE_UID]?.Value?.[0] === SOPInstanceUID)
  }

  const imageIds = instances.map((instanceMetaData) => {
    const seriesUid = instanceMetaData[SERIES_INSTANCE_UID].Value[0]
    const sopUid = SOPInstanceUID || instanceMetaData[SOP_INSTANCE_UID].Value[0]
    const imageId = `wadors:${wadoRsRoot}/studies/${StudyInstanceUID}/series/${seriesUid}/instances/${sopUid}/frames/1`
    dicomImageLoader.wadors.metaDataManager.add(imageId, instanceMetaData)
    return imageId
  })

  return imageIds
}

// 顶层：外部图像IDs加载为Volume，失败时回退为Stack
async function loadExternalImageIds(ids) {
  if (!ids || ids.length === 0) return
  const re = getRenderingEngine(renderingEngineId)
  const volId = props.volumeId || `DUAL_VIEW_${Date.now()}`
  // Qiniu: 预取并注入元数据，保证 Volume 构建所需的 modules 可用
  if (props.mode === 'qiniu') {
    await prefetchQiniuMetadata(ids)
    const firstMeta = metaData.get('imagePixelModule', ids[0])
    if (!firstMeta) {
      console.warn('[Meta] imagePixelModule 缺失，使用 Stack 渲染回退')
      console.warn('[Meta] imagePixelModule 缺失，使用 Stack 渲染回退:', ids[0])
    await renderStack(ids)
    return
  }
  }
  try {
    const volume = await volumeLoader.createAndCacheVolumeFromImages(volId, ids)
    await volume.load()
    await setVolumesForViewports(
      re,
      [
        {
          volumeId: volId,
          callback: setCtTransferFunctionForVolumeActor,
        },
      ],
      [viewportIdLeft, viewportIdRight]
    )
    re.render()
    currentVolumeId = volId
    isStackMode = false
  } catch (e) {
    console.error('Volume 构建失败，回退到 Stack 视图:', e)
    await renderStack(ids)
  }
}

// 顶层：Stack 回退渲染
async function renderStack(ids) {
  const re = getRenderingEngine(renderingEngineId)
  // 重新配置为 STACK 视口
  const stackViewports = [
    {
      viewportId: viewportIdLeft,
      type: ViewportType.STACK,
      element: vpLeftEl.value,
      defaultOptions: { background: [0, 0, 0] },
    },
    {
      viewportId: viewportIdRight,
      type: ViewportType.STACK,
      element: vpRightEl.value,
      defaultOptions: { background: [0, 0, 0] },
    },
  ]
  re.setViewports(stackViewports)
  const vpLeft = re.getViewport(viewportIdLeft)
  const vpRight = re.getViewport(viewportIdRight)
  await vpLeft.setStack(ids)
  await vpRight.setStack(ids)
  vpLeft.setImageIdIndex(0)
  vpRight.setImageIdIndex(0)
  // Stack 模式状态
  isStackMode = true
  stackIndex = 0
  stackCount = ids.length || 0
  // 同步左右缩放比例
  try {
    const z = vpRight.getZoom()
    vpLeft.setZoom(z)
  } catch (e) {}
  re.render()
}

async function loadSeriesIntoViewports(seriesDef) {
  if (props.mode === 'qiniu') {
    // 直接加载外部传入的 qiniu 图像
    await loadExternalImageIds(props.imageIds)
    return
  }
  let imageIds = []
  try {
    imageIds = await createImageIdsAndCacheMetaData({
      StudyInstanceUID: seriesDef.StudyInstanceUID,
      SeriesInstanceUID: seriesDef.SeriesInstanceUID,
    })
  } catch (e) {
    console.warn('request failed:', e)
    return
  }

  // Unique id per series
  const volumeId = `DUAL_VIEW_${seriesDef.SeriesInstanceUID}`
  const volume = await volumeLoader.createAndCacheVolumeFromImages(volumeId, imageIds)
  await volume.load()
  currentVolumeId = volumeId

  const re = getRenderingEngine(renderingEngineId)
  await setVolumesForViewports(
    re,
    [
      {
        volumeId,
        callback: setCtTransferFunctionForVolumeActor,
      },
    ],
    [viewportIdLeft, viewportIdRight]
  )

  re.render()
}

onMounted(async () => {
  await initEnv()

  // Tools setup（防御性：工具库不可用时跳过）
  let toolGroup = null
  try {
    if (cornerstoneTools?.addTool && ToolGroupManager?.createToolGroup) {
      cornerstoneTools.addTool(ReferenceLinesTool)
      cornerstoneTools.addTool(ZoomTool)

      // tool group
      toolGroup = ToolGroupManager.createToolGroup(toolGroupId)
      if (toolGroup?.addTool && toolGroup?.setToolEnabled) {
        toolGroup.addTool(ReferenceLinesTool.toolName, { sourceViewportId: viewportIdRight })
        toolGroup.addTool(ZoomTool.toolName)
        toolGroup.setToolEnabled(ReferenceLinesTool.toolName)
      }
    } else {
      console.warn('[Tools] cornerstone-tools 未可用，跳过工具初始化')
    }
  } catch (e) {
    console.warn('[Tools] 初始化失败，已跳过：', e)
  }

  // 检测WebGL支持并添加降级方案
  function checkWebGLSupport() {
    try {
      const canvas = document.createElement('canvas');
      const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
      const isSupported = !!(gl && gl.getContextAttributes());
      console.log('WebGL support detected:', isSupported);
      return isSupported;
    } catch (e) {
      console.warn('WebGL check failed:', e);
      return false;
    }
  }
  
  const isWebGLSupported = checkWebGLSupport();
  
  // Rendering engine and viewports
  const renderingEngine = new RenderingEngine(renderingEngineId)
  
  // 根据WebGL支持情况选择视口类型
  const viewportType = isWebGLSupported ? ViewportType.ORTHOGRAPHIC : ViewportType.STACK;
  console.log('Using viewport type:', viewportType);
  
  const viewportInputArray = [
    {
      viewportId: viewportIdLeft,
      type: viewportType,
      element: vpLeftEl.value,
      defaultOptions: {
        orientation: viewportType === ViewportType.ORTHOGRAPHIC ? Enums.OrientationAxis.CORONAL : undefined,
        background: [0, 0, 0],
      },
    },
    {
      viewportId: viewportIdRight,
      type: viewportType,
      element: vpRightEl.value,
      defaultOptions: {
        orientation: viewportType === ViewportType.ORTHOGRAPHIC ? Enums.OrientationAxis.AXIAL : undefined,
        background: [0, 0, 0],
      },
    },
  ]

  try {
    renderingEngine.setViewports(viewportInputArray);
    console.log('Viewports set successfully');
  } catch (e) {
    console.error('Failed to set viewports:', e);
    // 强制降级到STACK模式
    try {
      console.log('Attempting to use STACK viewport as fallback');
      const fallbackViewports = viewportInputArray.map(vp => ({
        ...vp,
        type: ViewportType.STACK,
        defaultOptions: {
          ...vp.defaultOptions,
          orientation: undefined
        }
      }));
      renderingEngine.setViewports(fallbackViewports);
    } catch (fallbackError) {
      console.error('Fallback to STACK viewport failed:', fallbackError);
    }
  }

  // bind tool group（防御性）
  if (toolGroup?.addViewport) {
    toolGroup.addViewport(viewportIdLeft, renderingEngineId)
    toolGroup.addViewport(viewportIdRight, renderingEngineId)
  }

  // 如果外部传入了图像ID，则优先使用外部数据进行加载，但仍显示侧边栏
  const hasExternalImages = props.imageIds && props.imageIds.length > 0
  if (hasExternalImages) {
    await loadExternalImageIds(props.imageIds)
  }

  // 创建左侧定位线
  locatorLineEl = document.createElement('div')
  locatorLineEl.className = 'locator-line'
  locatorLineEl.style.position = 'absolute'
  locatorLineEl.style.left = '0'
  locatorLineEl.style.right = '0'
  locatorLineEl.style.height = '0'
  locatorLineEl.style.borderTop = '2px solid #ffd54f'
  locatorLineEl.style.pointerEvents = 'none'
  locatorLineEl.style.zIndex = '10'
  vpLeftEl.value?.appendChild(locatorLineEl)

  function updateLocatorLine() {
    try {
      const re = getRenderingEngine(renderingEngineId)
      const vpRight = re.getViewport(viewportIdRight)
      if (!vpRight || !locatorLineEl) return
      let frac = 0.5
      if (isStackMode) {
        const denom = Math.max(1, stackCount - 1)
        frac = denom === 0 ? 0 : stackIndex / denom
      } else {
        const info = csUtils.getVolumeViewportScrollInfo(vpRight, currentVolumeId, true)
        const range = info?.sliceRangeInfo
        if (range && typeof range.current === 'number' && typeof range.min === 'number' && typeof range.max === 'number') {
          const denom = Math.max(1e-6, range.max - range.min)
          frac = (range.current - range.min) / denom
        }
      }
      const pct = Math.max(0, Math.min(100, (1 - frac) * 100))
      locatorLineEl.style.top = pct + '%'
    } catch (e) {}
  }

  // --- Right viewport: smooth zoom and slice scrolling ---
  let zoomAnimReq = null
  let sliceAnimReq = null
  let targetZoom = 1
  let targetSliceShift = 0

  function cancelAnimations() {
    if (zoomAnimReq != null) {
      try {
        cancelAnimationFrame(zoomAnimReq)
      } catch (e) {
        // 降级处理
        clearTimeout(zoomAnimReq)
      }
      zoomAnimReq = null
    }
    if (sliceAnimReq != null) {
      try {
        cancelAnimationFrame(sliceAnimReq)
      } catch (e) {
        // 降级处理
        clearTimeout(sliceAnimReq)
      }
      sliceAnimReq = null
    }
  }

  function animateZoom(viewport) {
    if (zoomAnimReq != null) {
      cancelAnimationFrame(zoomAnimReq)
    }
    const startZoom = viewport.getZoom()
    const startTime = performance.now()
    const duration = 150
    const easeOutQuad = (t) => 1 - (1 - t) * (1 - t)

    const step = () => {
      const now = performance.now()
      const t = Math.min(1, (now - startTime) / duration)
      const eased = easeOutQuad(t)
      const z = startZoom + (targetZoom - startZoom) * eased
      const clamped = Math.max(0.05, Math.min(50, z))
      viewport.setZoom(clamped)
      // 同步左视口缩放，保证左右图比一致
      try {
        const re = getRenderingEngine(renderingEngineId)
        const leftVp = re?.getViewport(viewportIdLeft)
        if (leftVp) {
          leftVp.setZoom(clamped)
        }
      } catch (e) {}
      viewport.render()
      if (t < 1) {
        zoomAnimReq = requestAnimationFrame(step)
      } else {
        zoomAnimReq = null
      }
    }
    zoomAnimReq = requestAnimationFrame(step)
  }
  
  function animateSliceShift(viewport, distanceAlongNormal) {
    if (sliceAnimReq != null) {
      try {
        cancelAnimationFrame(sliceAnimReq)
      } catch (e) {
        clearTimeout(sliceAnimReq)
      }
    }
    const camera = viewport.getCamera()
    const startFocal = camera.focalPoint.slice()
    const startPos = camera.position.slice()
    const normal = camera.viewPlaneNormal.slice()

    const startTime = performance.now()
    const duration = 150
    const easeOutQuad = (t) => 1 - (1 - t) * (1 - t)

    const step = () => {
      const now = performance.now()
      const t = Math.min(1, (now - startTime) / duration)
      const eased = easeOutQuad(t)
      const d = distanceAlongNormal * eased
      const newFocal = [
        startFocal[0] + normal[0] * d,
        startFocal[1] + normal[1] * d,
        startFocal[2] + normal[2] * d,
      ]
      const newPos = [
        startPos[0] + normal[0] * d,
        startPos[1] + normal[1] * d,
        startPos[2] + normal[2] * d,
      ]
      viewport.setCamera({ focalPoint: newFocal, position: newPos })
      viewport.render()
      updateLocatorLine()
      if (t < 1) {
        sliceAnimReq = requestAnimationFrame(step)
      } else {
        sliceAnimReq = null
      }
    }
    sliceAnimReq = requestAnimationFrame(step)
  }

  function setStackIndexBoth(newIndex) {
    const re = getRenderingEngine(renderingEngineId)
    const vpLeft = re.getViewport(viewportIdLeft)
    const vpRight = re.getViewport(viewportIdRight)
    stackIndex = Math.max(0, Math.min(stackCount - 1, newIndex))
    try { vpLeft.setImageIdIndex(stackIndex) } catch (e) {}
    try { vpRight.setImageIdIndex(stackIndex) } catch (e) {}
    try { re.render(); console.log('Rendering engine rendered successfully'); } catch (e) { console.error('Rendering failed:', e); }
    updateLocatorLine()
  }

  wheelHandler = (evt) => {
    const re = getRenderingEngine(renderingEngineId)
    const viewport = re.getViewport(viewportIdRight)
    if (!viewport) return
    evt.preventDefault()
    evt.stopPropagation()

    const delta = evt.deltaY || 0

    if (evt.altKey) {
      const current = viewport.getZoom()
      const factor = Math.exp(-delta * 0.0015)
      targetZoom = Math.max(0.05, Math.min(50, current * factor))
      cancelAnimations()
      animateZoom(viewport)
      return
    }
    // Stack 模式：使用图像索引进行切层
    if (isStackMode) {
      const step = delta > 0 ? 1 : -1
      setStackIndexBoth(stackIndex + step)
      return
    }

    // Volume 模式：沿法线平移摄像机实现切层
    const info = csUtils.getVolumeViewportScrollInfo(viewport, currentVolumeId, true)
    const spacing = info?.sliceRangeInfo?.spacingInNormalDirection ?? 1
    const speed = spacing * 0.5
    targetSliceShift = (delta > 0 ? 1 : -1) * speed
    cancelAnimations()
    animateSliceShift(viewport, targetSliceShift)
  }

  vpRightEl.value?.addEventListener('wheel', wheelHandler, { passive: false })

  const tip = document.createElement('div')
  tip.style.color = '#cfd8dc'
  tip.style.fontSize = '12px'
  tip.style.margin = '6px 0'
  tip.innerText = '提示：Alt + 滚轮 = 平滑缩放；滚轮 = 平滑切层；左键拖拽上下也可切层'
  sidebarEl.value?.appendChild(tip)

  vpRightEl.value?.addEventListener('mousedown', (e) => {
    if (e.button !== 0) return
    dragging = true
    lastY = e.clientY
  })
  window.addEventListener('mouseup', () => { dragging = false })
  window.addEventListener('mousemove', (e) => {
    if (!dragging) return
    const re = getRenderingEngine(renderingEngineId)
    const viewport = re.getViewport(viewportIdRight)
    if (!viewport) return
    const dy = e.clientY - lastY
    lastY = e.clientY
    // Stack 模式：拖拽转换为索引步进
    if (isStackMode) {
      const threshold = 4
      if (Math.abs(dy) >= threshold) {
        const step = dy > 0 ? 1 : -1
        setStackIndexBoth(stackIndex + step)
      }
      return
    }
    // Volume 模式：平移焦点沿法线切层
    const info = csUtils.getVolumeViewportScrollInfo(viewport, currentVolumeId, true)
    const spacing = info?.sliceRangeInfo?.spacingInNormalDirection ?? 1
    const k = spacing * 0.02
    const distance = -dy * k
    cancelAnimations()
    animateSliceShift(viewport, distance)
  })

  // Build sidebar items first, then update counts asynchronously（所有模式都显示）
  for (let i = 0; i < SERIES.length; i++) {
    const def = SERIES[i]

    const item = document.createElement('div')
    item.style.display = 'flex'
    item.style.alignItems = 'center'
    item.style.gap = '8px'
    item.style.padding = '8px'
    item.style.marginBottom = '8px'
    item.style.background = '#37474f'
    item.style.borderRadius = '4px'
    item.style.cursor = 'pointer'

    const thumb = document.createElement('div')
    thumb.style.width = '38px'
    thumb.style.height = '38px'
    thumb.style.background = '#000'
    thumb.style.border = '1px solid #607d8b'

    const label = document.createElement('div')
    label.style.flex = '1'
    label.innerText = `${def.title}  |  加载中...`

    const chevron = document.createElement('div')
    chevron.innerText = '▾'
    chevron.style.color = '#90a4ae'

    item.appendChild(thumb)
    item.appendChild(label)
    item.appendChild(chevron)
    item.onclick = () => loadSeriesIntoViewports(def)
    sidebarEl.value?.appendChild(item)

    // 异步获取计数，不阻塞渲染；qiniu 模式下跳过 dicomweb 请求以避免报错
    if (props.mode !== 'qiniu') {
      createImageIdsAndCacheMetaData({
        StudyInstanceUID: def.StudyInstanceUID,
        SeriesInstanceUID: def.SeriesInstanceUID,
      })
        .then((ids) => {
          label.innerText = `${def.title}  |  ${ids.length} image`
        })
        .catch((e) => {
          console.warn('request failed:', e)
          label.innerText = `${def.title}  |  未知`
        })
    } else {
      label.innerText = `${def.title}  |  未知`
    }
  }

  // 如果没有外部 imageIds，默认加载一个演示序列
  if (!hasExternalImages) {
    await loadSeriesIntoViewports(SERIES[1])
  }

  // 初始定位线
  updateLocatorLine()

  // 监听外部 imageIds 变化以触发加载
  watch(
    () => props.imageIds,
    async (ids) => {
      try {
        await loadExternalImageIds(ids)
      } catch (e) {
        console.error('外部图像IDs加载失败:', e)
      }
    },
    { deep: true }
  )
})

onBeforeUnmount(() => {
  try {
    vpRightEl.value?.removeEventListener('wheel', wheelHandler)
  } catch (e) {}
  dragging = false
})
</script>

<style scoped>
.dual-panel {
  display: flex;
  flex-direction: row;
  height: 800px;
}
.sidebar {
  width: 220px;
  background: #2b2b2b;
  color: #cfd8dc;
  padding: 8px;
  box-sizing: border-box;
  overflow-y: auto;
}
.main {
  flex: 1;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-gap: 6px;
  padding: 6px;
  background: #111;
}
.viewport {
  height: 100%;
  background: black;
  position: relative;
}
</style>